package appfile

import (
	"context"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
	"github.com/oam-dev/kubevela/apis/types"
	"github.com/oam-dev/kubevela/pkg/commands/util"
	"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
	"github.com/oam-dev/kubevela/pkg/utils/common"
)

const (
	// TerraformBaseLocation is the base directory to store all Terraform JSON files
	TerraformBaseLocation = ".vela/terraform/"
	// TerraformLog is the logfile name for terraform
	TerraformLog = "terraform.log"
)

// ApplyTerraform deploys addon resources
func ApplyTerraform(app *v1alpha2.Application, k8sClient client.Client, ioStream util.IOStreams, namespace string, dm discoverymapper.DiscoveryMapper) ([]v1alpha2.ApplicationComponent, error) {
	// TODO(zzxwill) Need to check whether authentication credentials of a specific cloud provider are exported as environment variables, like `ALICLOUD_ACCESS_KEY`
	var nativeVelaComponents []v1alpha2.ApplicationComponent
	// parse template
	appParser := NewApplicationParser(k8sClient, dm)
	appFile, err := appParser.GenerateAppFile(app.Name, app)
	if err != nil {
		return nil, fmt.Errorf("failed to parse appfile: %w", err)
	}
	if appFile == nil {
		return nil, fmt.Errorf("failed to parse appfile")
	}
	cwd, err := os.Getwd()
	if err != nil {
		return nil, err
	}

	for i, wl := range appFile.Workloads {
		switch wl.CapabilityCategory {
		case types.TerraformCategory:
			name := wl.Name
			ioStream.Infof("\nApplying cloud resources %s\n", name)

			tf, err := getTerraformJSONFiles(k8sClient, wl, appFile.Name, namespace)
			if err != nil {
				return nil, fmt.Errorf("failed to get Terraform JSON files from workload %s: %w", name, err)
			}

			tfJSONDir := filepath.Join(TerraformBaseLocation, name)
			if _, err = os.Stat(tfJSONDir); err != nil && os.IsNotExist(err) {
				if err = os.MkdirAll(tfJSONDir, 0750); err != nil {
					return nil, fmt.Errorf("failed to create directory for %s: %w", tfJSONDir, err)
				}
			}
			if err := ioutil.WriteFile(filepath.Join(tfJSONDir, "main.tf.json"), tf, 0600); err != nil {
				return nil, fmt.Errorf("failed to convert Terraform template: %w", err)
			}

			outputs, err := callTerraform(tfJSONDir)
			if err != nil {
				return nil, err
			}
			if err := os.Chdir(cwd); err != nil {
				return nil, err
			}

			outputList := strings.Split(strings.ReplaceAll(string(outputs), " ", ""), "\n")
			if outputList[len(outputList)-1] == "" {
				outputList = outputList[:len(outputList)-1]
			}
			if err := generateSecretFromTerraformOutput(k8sClient, outputList, name, namespace); err != nil {
				return nil, err
			}
		default:
			nativeVelaComponents = append(nativeVelaComponents, app.Spec.Components[i])
		}

	}
	return nativeVelaComponents, nil
}

func callTerraform(tfJSONDir string) ([]byte, error) {
	if err := os.Chdir(tfJSONDir); err != nil {
		return nil, err
	}
	var cmd *exec.Cmd
	cmd = exec.Command("bash", "-c", "terraform init")
	if err := common.RealtimePrintCommandOutput(cmd, TerraformLog); err != nil {
		return nil, err
	}

	cmd = exec.Command("bash", "-c", "terraform apply --auto-approve")
	if err := common.RealtimePrintCommandOutput(cmd, TerraformLog); err != nil {
		return nil, err
	}

	// Get output from Terraform
	cmd = exec.Command("bash", "-c", "terraform output")
	outputs, err := cmd.Output()
	if err != nil {
		return nil, err
	}
	return outputs, nil
}

// generateSecretFromTerraformOutput generates secret from Terraform output
func generateSecretFromTerraformOutput(k8sClient client.Client, outputList []string, name, namespace string) error {
	ctx := context.TODO()
	err := k8sClient.Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}})
	if err == nil {
		return fmt.Errorf("namespace %s doesn't exist", namespace)
	}
	var cmData = make(map[string]string, len(outputList))
	for _, i := range outputList {
		line := strings.Split(i, "=")
		if len(line) != 2 {
			return fmt.Errorf("terraform output isn't in the right format")
		}
		k := strings.TrimSpace(line[0])
		v := strings.TrimSpace(line[1])
		if k != "" && v != "" {
			cmData[k] = v
		}
	}

	objectKey := client.ObjectKey{
		Namespace: namespace,
		Name:      name,
	}
	var secret v1.Secret
	if err := k8sClient.Get(ctx, objectKey, &secret); err != nil && !errors.IsNotFound(err) {
		return fmt.Errorf("retrieving the secret from cloud resource %s hit an issue: %w", name, err)
	} else if err == nil {
		if err := k8sClient.Delete(ctx, &secret); err != nil {
			return fmt.Errorf("failed to store cloud resource %s output to secret: %w", name, err)
		}
	}

	secret = v1.Secret{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "v1",
			Kind:       "Secret",
		},
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
			Name:      name,
		},
		StringData: cmData,
	}

	if err := k8sClient.Create(ctx, &secret); err != nil {
		return fmt.Errorf("failed to store cloud resource %s output to secret: %w", name, err)
	}
	return nil
}

// getTerraformJSONFiles gets Terraform JSON files or modules from workload
func getTerraformJSONFiles(k8sClient client.Client, wl *Workload, applicationName string, namespace string) ([]byte, error) {
	pCtx, err := PrepareProcessContext(k8sClient, wl, applicationName, namespace)
	if err != nil {
		return nil, err
	}
	base, _ := pCtx.Output()
	tf, err := base.Compile()
	if err != nil {
		return nil, err
	}
	return tf, nil
}
